Skip to content
View Article Network

Applying MemoryCache in ASP.NET MVC

Using ActionFilter to Cache Action Content

OutputCacheAttribute is an ActionFilter provided by MVC used to mark Action Methods for caching. If OutputCache is not specifically configured, the server-side cache of OutputCacheAttribute is implemented using MemoryCache.

Properties

  • Duration: The duration of the cache (in seconds).

  • Location: The storage location for the cache. Please refer to "OutputCacheLocation" for setting values. A brief summary is provided below:

    • None: Disables caching.
    • Client: Browser client.
    • Server: Web Server.
    • Downstream: Client and Proxy Server.
    • ServerAndClient: Client and Web Server.
    • Any: Web Server, Client, and Proxy Server.
  • NoStore: Sets whether caching is disallowed.

  • VaryByXXX: Differentiates cached content based on Headers, Form, and Query parameters. For example, when performing report queries, caching should be configured based on different query conditions.

  • CacheProfile: Sets the Name of the caching scheme defined in the Config. Typically, a project will have several fixed caching schemes. To avoid modifying all code using a scheme when its content changes, cache settings are defined in the Config, and each Action Method uses CacheProfile to specify the scheme.

TIP

NoStore and Location.None look similar but have different actual effects. The specific behaviors are as follows:

  • NoStore: Sets the Cache-Control header to no-store, which does not affect Web Server caching.
  • Location.None: Sets the Cache-Control header to no-cache and does not store the Web Server cache.

For the behavior of Cache-Control, please refer to "Cache-Control". Excerpts for no-store and no-cache are as follows: no-store: Do not let the browser cache. no-cache: Use the cache, but check with the server for new content before every request.

For the execution results of each Location setting, you can refer to this article by Darkthread: "In-depth observation of ASP.NET OutputCache behavior".

Practical Use Case

For pages with pure data query functionality (which should not contain links to edit pages—I have encountered cases where the cache was 30 seconds, and a user quickly went to the edit page, modified data, returned, and was confused why the data hadn't changed), if some queries are time-consuming, you can use OutputCacheAttribute to create cached data.

Regarding caching for query functions, you can use VaryByParam="*" to create different cache versions based on different QueryString or POST parameters. However, if permissions are set for different users, this will cause users with different permissions to cache the same result. Therefore, additional handling is required. Below is an implementation example.

Web.config

Please set the duration in seconds according to your project requirements.

xml
<system.web>
  <caching>
    <outputCacheSettings>
      <outputCacheProfiles>
        <add name="Default" duration="30" varyByParam="*" varyByCustom="Cookie" noStore="true" />
      </outputCacheProfiles>
    </outputCacheSettings>
  </caching>
</system.web>

Global.asax.cs

Override GetVaryByCustomString() to generate different Keys for different users. Two methods using Cookie and Session are provided below. If using Session, you must change the varyByCustom value in "Web.config" to Session.

csharp
public class MvcApplication : HttpApplication {
    public override string GetVaryByCustomString(HttpContext context, string custom) {
        const string OutputCacheKey = "OutputCacheId";

        if (custom.Equals("Cookie", StringComparison.OrdinalIgnoreCase)) {
            if (Request.Cookies[OutputCacheKey] == null) {
                string cacheId = Guid.NewGuid().ToString();
                Response.Cookies.Add(new HttpCookie(OutputCacheKey) {
                    Value = cacheId,
                    HttpOnly = true,
                    Expires = DateTime.Now.AddHours(1),
                    Secure = false // Please set based on whether SSL is used
                });

                return cacheId;
            }

            return Request.Cookies[OutputCacheKey].Value;
        }

        // Please replace UserId with the actual Session Key of the logged-in account
        if (custom.Equals("Session", StringComparison.OrdinalIgnoreCase)
            && Session["UserId"] != null
        ) {
            string userId = Session["UserId"].ToString();

            if (Session[OutputCacheKey] == null
                || !(Session[OutputCacheKey] is VaryByCustomInfo customInfo)
                || customInfo.UserId == userId
            ) {
                Guid value = Guid.NewGuid();
                Session[OutputCacheKey] = new VaryByCustomInfo(userId, value);
                return value.ToString();
            }

            return customInfo.Value.ToString();
        }

        return base.GetVaryByCustomString(context, custom);
    }

    private class VaryByCustomInfo {
        public VaryByCustomInfo(string userId, Guid value) {
            UserId = userId ?? throw new ArgumentNullException(nameof(userId));
        }

        public string UserId { get; }

        public Guid Value { get; }
    }
}

Controller

You can set a breakpoint in Index to test. You will find that as long as the model content is the same and the same person is browsing the page, the breakpoint will only be triggered on the first visit within a certain period.

csharp
public class TestController : Controller {
    [OutputCache(CacheProfile = "Default")]
    [HttPost]
    public ActionResult Index(IndexViewModel model) {
        //...Implementation to generate ActionResult using model...
    }
}

Clearing Cache Data When Updating the Database

Some data that is small in volume and rarely changes can be stored in the cache to reduce the number of database connections, such as city/county data and website settings. To avoid having stale data in the cache when data changes, you must clear or update the cache in the API that modifies the data. However, if you modify the database directly, there is still a possibility of caching stale data. Therefore, the best practice is to monitor the database directly and clear the cache when data changes.

ChangeMonitor

MemoryCache can use ChangeMonitor to detect whether the data source has changed. The .NET Framework provides the following two implementation classes:

  • HostFileChangeMonitor: Used to monitor file changes on the host.
  • SqlChangeMonitor: Used to detect data changes on SQL Server. SqlChangeMonitor uses "SqlDependency" to monitor database data changes. When SqlDependency is added to a SqlCommand, it creates a "SqlNotificationRequest" assigned to the SqlCommand to establish a notification request with SQL Server. When data is modified, SqlChangeMonitor notifies MemoryCache to clear the cached data.

Example

Global.asax.cs

csharp
public class MvcApplication : System.Web.HttpApplication {
    protected void Application_Start() {
        //...Other implementations in Application_Start...

       // Add this line
       SqlDependency.Start(WebConfigurationManager.ConnectionStrings["MyDB"].ConnectionString);
    }

    protected void Application_End() {
        // Add this line
        SqlDependency.Stop(WebConfigurationManager.ConnectionStrings["MyDB"].ConnectionString);
    }
}

HomeController

csharp
public class HomeController : Controller {
    private static DateTime lastChangedTime;

    private const string CacheKey = "CacheKey";

    public ActionResult TestDependency() {
        // If no cache data, create it
        if (MemoryCache.Default[CacheKey] is null) {
            CreateCache();
        }
        ViewBag.Key1 = MemoryCache.Default[CacheKey] as string;
        ViewBag.LastChangedTime = lastChangedTime;

        return View();
    }

    private void CreateCache() {
        string connectionStr = WebConfigurationManager.ConnectionStrings["MyDB"].ConnectionString;

        CacheItemPolicy policy = new CacheItemPolicy();
        using (SqlConnection conn = new SqlConnection(connectionStr))
        using (SqlCommand cmd = new SqlCommand("SELECT Key1 FROM dbo.Config", conn)) {
            SqlDependency dependency = new SqlDependency(cmd);
            // Can use OnChange to update other data when data changes
            dependency.OnChange += SqlDependencyOnChange;

            conn.Open();
            // Must execute the SQL Command once for the monitoring to take effect; you can decide whether to fetch data simultaneously
            string key1 = cmd.ExecuteScalar().ToString();

            SqlChangeMonitor monitor = new SqlChangeMonitor(dependency);
            policy.ChangeMonitors.Add(monitor);

            // Set cache data; clear cache when data changes
            MemoryCache.Default.Set(CacheKey, key1, policy);
        }
    }

    private void SqlDependencyOnChange(object sender, SqlNotificationEventArgs e) {
        lastChangedTime = DateTime.Now;
        (sender as SqlDependency).OnChange -= SqlDependencyOnChange;
    }
}

WARNING

  • To enable data monitoring, the Service Broker feature must be enabled in the database.
  • The SQL syntax used for monitoring must specify the exact columns to be monitored, and the table name must include the Schema (e.g., dbo), otherwise, the cache data cannot be created correctly.
  • After setting SqlCommand for SqlDependency, you must execute the SqlCommand once for it to take effect.

Enabling Service Broker

If Service Broker is not yet enabled, you can enable it using the following syntax:

sql
ALTER DATABASE {DatabaseName} SET ENABLE_BROKER;

If you detach and re-attach a database that already has Service Broker enabled, executing this syntax might result in the following error message:

text
Service Broker cannot be enabled in database "<DBName>" because the Service Broker GUID in the database (<GUID>) does not match the one in sys.databases (<GUID>).

In this case, you need to reset the Service Broker using the following syntax:

sql
ALTER DATABASE {DatabaseName} SET NEW_BROKER;

Since enabling Service Broker must be done while no other users are connected, if it is a running database, you should add WITH ROLLBACK IMMEDIATE when executing the above syntax to roll back incomplete transactions and disconnect other users from the database. Therefore, the complete syntax is as follows:

sql
ALTER DATABASE {DatabaseName} SET NEW_BROKER WITH ROLLBACK IMMEDIATE;

Change History

    • Initial version created.